Fix Android Fabric Native Animated null prop crash#56913
Conversation
|
@zeyap has imported this pull request. If you are a Meta employee, you can view this in D105955122. |
| } | ||
|
|
||
| if (propKey == PROP_TRANSFORM) { | ||
| assert(outputReadableMap.getType(propKey) == ReadableType.Array && propValue is List<*>) |
There was a problem hiding this comment.
In fact if Fabric commit includes a null value for transform that means you're doing a setState on other prop on this view, override on transform shouldn't be cleared because of that
I'd recommend not doing the change on lines 1332-1343 and let this check allow null from outputReadableMap
val outputType = outputReadableMap.getType(propKey)
assert(
(outputType == ReadableType.Array || outputType == ReadableType.Null) &&
propValue is List<*>
)
There was a problem hiding this comment.
Also here
val outputType = outputReadableMap.getType(propKey)
assert(
(outputType == ReadableType.Number || outputType == ReadableType.Null) &&
propValue is Number
)
could you try and see if animation behavior is still expected?
| if (directPropsMap.isEmpty()) { | ||
| tagToSynchronousMountProps.remove(reactTag) | ||
| } |
There was a problem hiding this comment.
this is not right, tagToSynchronousMountProps will be cleared after 1 regular Fabric commit
|
to clarify
|
8189b14 to
26a466a
Compare
|
I tried this direction and I think the behavior matches what you described. Normal Fabric So I moved the reset to the synchronous path instead This seems closer to the iOS behavior too: regular Fabric commits do not overwrite animated-managed props, but the synchronous animated path can still reset them. |
| val synchronousMountProps = tagToSynchronousMountProps[reactTag] ?: mutableMapOf() | ||
| removeNullPropsFromPropsReadableMap(props, synchronousMountProps) | ||
| synchronousMountProps.putAll(propsMap) | ||
| if (synchronousMountProps.isEmpty()) { | ||
| tagToSynchronousMountProps.remove(reactTag) | ||
| } else { | ||
| synchronousMountProps = propsMap | ||
| tagToSynchronousMountProps[reactTag] = synchronousMountProps | ||
| } |
There was a problem hiding this comment.
I would recommend keeping this function storeSynchronousMountPropsOverride as is.
you're now removing reactTag from tagToSynchronousMountProps if tagToSynchronousMountProps[reactTag] is empty, I think it's okay to keep but may be no-op
adding removeNullPropsFromPropsReadableMap might cause issue, some prop's (not transform) null value could be actually meaningful. updateProps can take all types of viewprops so i'd be cautious
There was a problem hiding this comment.
hm, i guess the issue is if we dont clear the null props, restore doesnt really clear override as you expected?
if you see this issue, for the short term we can do this: if opacity and transform receive null prop, clear them; for other props we keep null as is, there probably should be an API to explicitly clear a prop from override map
There was a problem hiding this comment.
Yes, exactly. If restoreDefaultValues() sends transform: null but the stored synchronous override is not cleared, the restore only updates the view once. The old override can still remain in tagToSynchronousMountProps and win again on a later regular Fabric commit.
That is why I originally handled both pieces together.
restoreDefaultValues()sends a synchronous null update for the animated props it owns.- The synchronous override store treats that null as a clear signal and removes the stored override key.
I agree that generic null removal is too broad, though. We can scope the clear logic only to the props that this override map actually stores today (transform and opacity), instead of removing every null prop key.
Given that the restore behavior depends on clearing the stored override, do you think it would be better to keep both changes in this PR, with the null-clear logic scoped to transform / opacity?
There was a problem hiding this comment.
If you prefer keeping this PR focused only on the regular Fabric updateProps null handling, I can split the restore behavior into a follow-up PR now.
In that case, I’ll remove the restoreDefaultValues() change and the null-clear logic from this PR, and keep this one limited to allowing stored synchronous transform / opacity overrides to win over regular Fabric null updates.
There was a problem hiding this comment.
That makes sense. I’ll go with the short-term scoped fix here.
I’ll keep the null-clear behavior limited to the props that are currently stored in this override map: transform and opacity. So synchronous transform: null / opacity: null can clear the stored override for restore, while null values for other props are left untouched.
I agree that a more explicit API for clearing a prop from the override map would be cleaner longer term. I’ll update the patch in this direction and push again.
There was a problem hiding this comment.
Yes let's scope clear logic to transform and opacity
Let's move restoreDefaultValues to another change because I'm concerned that restore was no-op, so adding things here will regress existing apps.
I realize restoreDefaultValues is not fired at reset(), but you mentioned resetting actually restores the value
| connectedViewTag = -1 | ||
| } | ||
|
|
||
| fun restoreDefaultValues() { |
There was a problem hiding this comment.
thanks for adding this I think java NativeAnimated misses impl for restore
but could you put it in a separate PR?
| val outputType = outputReadableMap.getType(propKey) | ||
| assert( | ||
| (outputType == ReadableType.Array || outputType == ReadableType.Null) && | ||
| propValue is List<*>) |
26a466a to
d25ff0d
Compare
d25ff0d to
8fc40a7
Compare
|
Thanks for the helpful review 🙇♂️🙇♂️🙇♂️ I updated the PR based on your feedback and also refreshed the repro evidence. One thing I want to confirm: from the product behavior perspective, the expected result after clearing the animated Do you already have a plan for handling the restore behavior separately, or would you prefer me to follow up with a separate PR for that? |
Summary
This fixes an Android Fabric crash in the
overrideBySynchronousMountPropsAtMountingAndroidpath when Native Animated has stored a synchronoustransform/opacityoverride and a later regular Fabric commit containsnullfor the same prop.Native Animated can synchronously update
transform/opacityon the UI thread. A regular Fabric commit for the same view may arrive later with stale props, includingtransform: nulloropacity: null. Those regular Fabric null values should not clear the stored synchronous override, because the override may still represent the fresher Native Animated value.Before this patch, the override merge path assumed that the incoming Fabric prop had the same non-null type as the stored override. For example, a stored
transformoverride is an array, but a regular Fabric update may containtransform: null. That mismatch could crash inSurfaceMountingManager.overridePropsReadableMap.This patch:
nullupdates to be overridden by stored synchronoustransform/opacityvalues instead of crashing.transform: null/opacity: nullfrom clearing the stored synchronous override.transform/opacityonly.This PR intentionally does not change Java Native Animated
restoreDefaultValues()behavior. That restore implementation can be handled separately because changing it may affect existing apps.Repro repository:
https://github.com/jingjing2222/react-native-fabric-transform-repro
Why this is correct
A regular Fabric
nullupdate is not enough to prove user intent to clear the native animated value, because it can be the stale commit produced after Native Animated already wrote a fresher value on the UI thread. In that case, the stored synchronous override should continue to win.If the synchronous Native Animated path explicitly sends
transform: nulloropacity: null, this patch treats that as a clear signal for that stored override key. The clear behavior is intentionally scoped to the props currently stored by this override path.Changelog:
[ANDROID] [FIXED] - Fix Android Fabric Native Animated crash when regular Fabric null props collide with stored synchronous transform or opacity overrides.
Test Plan:
Added regression coverage for stale regular Fabric null commits and scoped synchronous null clears:
updateProps_withNullOpacity_keepsStoredSynchronousPropupdateProps_withNullTransform_keepsStoredSynchronousPropupdatePropsSynchronously_withNullOpacity_removesStoredSynchronousPropupdatePropsSynchronously_withNullTransform_removesStoredSynchronousPropRan:
Result:
Ran:
Result:
Ran:
Result: